package edu.zju.gis.dbfg.model.tile;

import com.google.gson.Gson;
import com.google.gson.internal.LinkedTreeMap;
import edu.zju.gis.dbfg.model.PyramidConfig;
import edu.zju.gis.dbfg.model.base.Pipeline;
import edu.zju.gis.dbfg.model.base.PipelineBuilder;
import edu.zju.gis.dbfg.model.base.VectorTileBuilder;
import edu.zju.gis.dbfg.model.io.geojson.GeoJsonTileBuilder;
import edu.zju.gis.dbfg.model.io.pbf.MVTTileBuilder;
import edu.zju.gis.dbfg.model.util.GeoItem;
import edu.zju.gis.dbfg.model.util.TileID;
import edu.zju.gis.dbfg.model.util.TileUtil;
import lombok.Getter;
import lombok.Setter;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.GeoShapeQueryBuilder;
import org.elasticsearch.search.Scroll;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
import org.locationtech.jts.simplify.TopologyPreservingSimplifier;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import scala.Tuple2;

import java.awt.*;
import java.io.Serializable;
import java.util.*;
import java.util.List;

import static org.elasticsearch.index.query.QueryBuilders.geoShapeQuery;

@Getter
@Setter
public class TileJob implements Serializable {

    public final static Integer SCREEN_TILE_SIZE = 256;
    public final static Integer OVERSAMPLE_FACTOR = 1;
    // public final static double SIMPLYFY_PARAM = 0.25;

    private final static Integer BUFFER = 1;
    private final static Integer SEARCH_SCROLL_SIZE = 2000;
    private final static long SEARCH_SCROLL_INTERVAL = 5L;
    private final static String SEARCH_ES_IP = "10.2.40.51, 10.2.40.52, 10.2.40.53, 10.2.40.54";
    private final static int SEARCH_ES_PORT = 9200;


    private static final Logger logger = LoggerFactory.getLogger(TileJob.class);

    private int partition;
    private String[] source;
    private int zMax;
    private int zMin;
    private PyramidConfig pyramidConfig;
    private CoordinateReferenceSystem sourceCrs;
    private CoordinateReferenceSystem targetCrs;
    private String layerName;
    private String fileType;
    private String outdir;

    public TileJob() {
        this.partition = 8;
        this.zMax = 12;
        this.zMax = 6;
    }

    public List<Tuple2<TileID, GeoItem>> tileJobResult(TileID tileID) throws Exception {

        String[] ips = SEARCH_ES_IP.split(",");
        HttpHost[] hosts = new HttpHost[ips.length];
        for (int m=0; m<ips.length; m++) {
            hosts[m] = new HttpHost(ips[m].trim(), SEARCH_ES_PORT);
        }
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(hosts));

        List<Tuple2<TileID, GeoItem>> result = new ArrayList<>();


        /**
         * 查询 es 获取当前格网内部的数据
         */
        try {
            List<Tuple2<String, String>> tuples = new ArrayList<>();
            Envelope tile = TileUtil.createTileBox(tileID, pyramidConfig);

            MathTransform tstransform = CRS.findMathTransform(targetCrs, sourceCrs);
            MathTransform sttransform = CRS.findMathTransform(sourceCrs, targetCrs);

            tile = JTS.transform(tile, tstransform);
            // logger.info(String.format("Transformed tile for search: " + tileID.toString() + ", " + tile.toString()));

            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            GeoShapeQueryBuilder qb = geoShapeQuery(
                    "shape",
                    new EnvelopeBuilder(new Coordinate(tile.getMinX(), tile.getMaxY()), new Coordinate(tile.getMaxX(), tile.getMinY())));
            qb.relation(ShapeRelation.INTERSECTS);
            sourceBuilder.query(qb);
            sourceBuilder.size(SEARCH_SCROLL_SIZE);
            // first search request to get one batch of data
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices(source);
            searchRequest.source(sourceBuilder);
            searchRequest.scroll(TimeValue.timeValueMinutes(SEARCH_SCROLL_INTERVAL));
            SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
            SearchHits hits = searchResponse.getHits();
            SearchHit[] searchHits = hits.getHits();
            for (SearchHit hit : searchHits) {
                tuples.add(new Tuple2<>(hit.getId(), hit.getSourceAsString()));
            }

            // scroll request to get all left relevant data
            String scrollId = searchResponse.getScrollId();
            final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(SEARCH_SCROLL_INTERVAL));
            while (searchHits != null && searchHits.length > 0) {
                SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
                scrollRequest.scroll(scroll);
                searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
                scrollId = searchResponse.getScrollId();
                searchHits = searchResponse.getHits().getHits();
                for (SearchHit hit : searchHits) {
                    tuples.add(new Tuple2<>(hit.getId(), hit.getSourceAsString()));
                }
            }

            // clear scroll id
            ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
            clearScrollRequest.addScrollId(scrollId);

            client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
            client.close();

            if (tuples.size() == 0) {
                return result;
            }

            logger.info(String.format("TileID %s, Tuples count: %d", tileID.toString(), tuples.size()));

            /**
             * 对于获取到的数据,转为GeoItem
             */
            List<GeoItem> features = new ArrayList<>();
            Envelope tileEnvelope = TileUtil.createTileBox(tileID, pyramidConfig);
            Pipeline clipPipeline = getPipeline(targetCrs, BUFFER, tileEnvelope, false, true, false);

            for (Tuple2<String, String> f: tuples) {
                Gson gson = new Gson();
                Map<String, Object> inMap = gson.fromJson(f._2, new HashMap<String, Object>().getClass());
                inMap.remove("id");
                if (!inMap.containsKey("shape")) {
                    logger.error("Unvalid geometry item for id " + f._1 + "with source " + source + ", must have field named with 'shape'");
                    continue;
                }
                Map<String, Object> geoMap = (LinkedTreeMap) inMap.get("shape");
                String type = (String) geoMap.get("type");
                ArrayList t = ((ArrayList) geoMap.get("coordinates"));
                GeometryFactory gf = new GeometryFactory();
                for (int i=0; i<t.size(); i++) {
                    List<List<ArrayList>> coordinates = new ArrayList<>();
                    ArrayList<ArrayList> m = (ArrayList<ArrayList>)t.get(i);
                    if (!type.contains("Multi")) {
                        coordinates.add(m);
                    } else {
                        for (int x=0; x<m.size(); x++) {
                            coordinates.add(m.get(x));
                        }
                    }
                    for (List<ArrayList> coordinate: coordinates) {
                        Geometry geometry;
                        if (type.equals("Polygon") || type.equals("MultiPolygon")) {
                            Coordinate[] cs = coordinate.stream().map((p)->new Coordinate((double)(p.get(0)), (double)(p.get(1)))).toArray(Coordinate[]::new);
                            geometry = gf.createPolygon(cs);
                        } else if (type.equals("LineString") || type.equals("MultiLineString")) {
                            Coordinate[] cs = coordinate.stream().map((p)->new Coordinate((double)(p.get(0)), (double)(p.get(1)))).toArray(Coordinate[]::new);
                            geometry = gf.createLineString(cs);
                        } else {
                            // type == Point
                            Coordinate c = new Coordinate((double)(coordinate.get(0).get(0)), (double)(coordinate.get(0).get(1)));
                            geometry = gf.createPoint(c);
                        }
                        GeoItem geoItem = new GeoItem();
                        geoItem.setFid(f._1);
                        inMap.remove("shape");
                        // geoItem.setProp(inMap);
                        // 基于当前层级瓦片抽稀
                        // double tolerance = 0.25 * (pyramidConfig.getGridSize(tileID.getzLevel())/SCREEN_TILE_SIZE);
                        // geometry = simplify(geometry, tolerance);
                        geometry = JTS.transform(geometry, sttransform);
//                        geoItem.setGeometry(geometry);
//                        features.add(geoItem);
                        geometry = clipPipeline.execute(geometry);
                        if (!geometry.isEmpty()) {
                            geometry = onlyPolygon(geometry);
                            if (geometry != null) {
                                geoItem.setGeometry(geometry);
                                features.add(geoItem);
                            }
                        }
                    }
                }
            }

            logger.info(String.format("TileID %s, Features count: %d", tileID.toString(), features.size()));

            /**
             * 计算当前瓦片号在当前层级及更低层级的瓦片，并输出
             */
            generateLowerLevelsTiles(tileID, features, zMax, pyramidConfig, layerName, fileType, outdir);

            /**
             * 抽稀当前层级 geometry
             */
            Pipeline pipeline = getPipeline(targetCrs, BUFFER, tileEnvelope, false, false, true);
            List<GeoItem> tFs = new ArrayList<>();
            for (GeoItem gi: features) {
                Geometry g = gi.getGeometry();
                g = pipeline.execute(g);
                if (!g.isEmpty()) {
                    g = onlyPolygon(g);
                    if (g != null) {
                        gi.setGeometry(g);
                        tFs.add(gi);
                    }
                }
            }
            /**
             * 输出当前层级瓦片
             */
            buildTile(tFs, tileID, pyramidConfig, layerName, fileType, outdir);

            /**
             * 处理数据用于上一层计算，这时就不需要clip了，只需要进行抽稀即可
             */
            if (tileID.getzLevel() > zMin) {
                /**
                 * 计算当前瓦片更高一层级的瓦片ID
                 */
                TileID upperTileID = new TileID();
                upperTileID.setzLevel(tileID.getzLevel()-1);
                upperTileID.setX(tileID.getX()/2);
                upperTileID.setY(tileID.getY()/2);
                Envelope upperLevelEnvelope = TileUtil.createTileBox(upperTileID, pyramidConfig);
                Pipeline upperLevelSimplifyPipeline = getPipeline(targetCrs, BUFFER, upperLevelEnvelope, false, false, true);
                for (int i=0; i<tFs.size(); i++) {
                    GeoItem gi = tFs.get(i);
                    Geometry g = upperLevelSimplifyPipeline.execute(gi.getGeometry());
                    if (g.isEmpty()) {
                        continue;
                    }
                    g = onlyPolygon(g);
                    if (g == null) {
                        continue;
                    }
                    gi.setGeometry(g);
                    result.add(new Tuple2<>(upperTileID, gi));
                }
            } else {
                result.add(new Tuple2<>(null, null));
            }

            logger.info(String.format(" ======== Tile %s stop, count:  %d ======== ", tileID, result.size()));
            return result;
        } catch (Exception e) {
            logger.error(String.format("Tile %s fail: %s", tileID.toString(), e.getMessage()));
            e.printStackTrace();
        } finally {
            client.close();
        }
        return result;
    }

    public void buildTile(List<GeoItem> features, TileID tileID, PyramidConfig pyramidConfig, String layerName, String fileType, String outDir) throws Exception {
        Envelope tileEnvelope = TileUtil.createTileBox(tileID, pyramidConfig);
        VectorTileBuilder vectorTileBuilder;
        Pipeline prePipeline = getPipeline(pyramidConfig.getCrs(), BUFFER, tileEnvelope, true, false, false);
        if (fileType.equals("geojson")) {
            vectorTileBuilder = new GeoJsonTileBuilder(outDir, tileID, pyramidConfig);
        } else {
            vectorTileBuilder = new MVTTileBuilder(outDir, tileID, tileEnvelope, layerName);
        }
        for (GeoItem feature: features) {
            Geometry tileGeometry = prePipeline.execute(feature.getGeometry());
            if (tileGeometry.isEmpty())
                continue;

            tileGeometry = onlyPolygon(tileGeometry);

            if (tileGeometry == null)
                continue;

            vectorTileBuilder.addFeature(layerName, feature.getFid(), feature.getFid(),
                    tileGeometry, feature.getProp());
        }

        vectorTileBuilder.build();
    }

    /**
     * 迭代计算指定层级及更低层级的瓦片信息
     * @param tileID
     * @param features
     * @param zMax
     * @param pyramidConfig
     * @param layerName
     * @param fileType
     * @param outDir
     * @throws Exception
     */
    public boolean generateLowerLevelsTiles(TileID tileID, List<GeoItem> features, int zMax, PyramidConfig pyramidConfig, String layerName, String fileType, String outDir) throws Exception {
        List<TileID> allLowerLevelTiles = new ArrayList<>();
        int z = tileID.getzLevel();
        int x = tileID.getX();
        int y = tileID.getY();
        int zLower = z+1;
        while (zLower<=zMax) {
            int xmin = x * (int) Math.pow(2, zLower-z);
            int xmax = ((x+1) * (int) Math.pow(2, zLower-z))-1;
            int ymin = y * (int) Math.pow(2, zLower-z);
            int ymax = ((y+1) * (int) Math.pow(2, zLower-z))-1;
            for (int i=xmin; i<=xmax; i++) {
                for (int j=ymin; j<=ymax; j++) {
                    TileID l = new TileID();
                    l.setX(i);
                    l.setY(j);
                    l.setzLevel(zLower);
                    allLowerLevelTiles.add(l);
                }
            }
            zLower++;
        }
        logger.info(String.format("Origin Tile ID: %s, Lower level tile count: %d", tileID.toString(), allLowerLevelTiles.size()));
        for (TileID t: allLowerLevelTiles) {
            Envelope tileEnvelope = TileUtil.createTileBox(t, pyramidConfig);
            Pipeline pipeline = getPipeline(targetCrs, BUFFER, tileEnvelope, false, true, true);
            List<GeoItem> tFs = new ArrayList<>();
            for (GeoItem gi: features) {
                Geometry g = gi.getGeometry();
                try {
                    g = pipeline.execute(g);
                    if (!g.isEmpty()) {
                        g = onlyPolygon(g);
                        if (g != null) {
                            GeoItem tf = new GeoItem();
                            tf.setFid(gi.getFid());
                            tf.setGeometry(g);
                            tFs.add(tf);
                        }
                    }
                } catch (Exception e) {
                    logger.error("Unvalid geometry: " + g.toText());
                    logger.error(e.getMessage());
                }
            }
            if (tFs.size() == 0) continue;
            buildTile(tFs, t, pyramidConfig, layerName, fileType, outDir);
            logger.info(String.format("Tile %s success with total feature count %d", t.toString(), tFs.size()));
        }
        return true;
    }

    /**
     * 简化图像
     * @param geo
     * @param distanceTolerance
     * @return
     */
    public static Geometry simplify(Geometry geo, double distanceTolerance) {
        switch (geo.getDimension()) {
            case 2:
                return TopologyPreservingSimplifier.simplify(geo, distanceTolerance);
            case 1:
                return DouglasPeuckerSimplifier.simplify(geo, distanceTolerance);
            default:
                logger.warn("Unvalid geometry type for geometry simplify");
                return geo;
        }
    }

    /**
     * 根据阈值过滤小图斑
     * @param geo
     * @param span
     * @return
     */
    public static boolean smallGeomeryFilter(Geometry geo, double span) {
        double area = span * span;
        if (geo.getDimension() > 0) {
            Envelope e = geo.getEnvelopeInternal();
            if (Math.max(e.getWidth(), e.getHeight()) < span) {
                return false;
            }
            if (e.getArea() < area) {
                return false;
            }
            return true;
        } else {
            logger.error("Unvalid geometry type for small geometry filter");
            return false;
        }
    }

    /**
     * 构建瓦片中每个像素处理的Pipeline
     * 默认目标crs与源crs一致
     */
    public Pipeline getPipeline(CoordinateReferenceSystem sourceCrs, int buffer, Envelope referenceEnvelop, boolean preprocess, boolean isClip, boolean isSimplify) {
        CoordinateReferenceSystem targetCrs = sourceCrs;
        int mapWidth = SCREEN_TILE_SIZE;
        int mapHeight = SCREEN_TILE_SIZE;
        int overSampleFactor = OVERSAMPLE_FACTOR;
        // rectangle of the image: width and height
        Rectangle paintArea = new Rectangle(mapWidth, mapHeight);

        // bounding box - in final map (target) CRS
        ReferencedEnvelope renderingArea = new ReferencedEnvelope(referenceEnvelop.getMinX(), referenceEnvelop.getMaxX(), referenceEnvelop.getMinY(), referenceEnvelop.getMaxY(), targetCrs);
        Pipeline pipeline = null;

        try {
            final PipelineBuilder builder =
                    PipelineBuilder.newBuilder(
                            renderingArea, paintArea, sourceCrs, overSampleFactor, buffer);

            if (preprocess){
                pipeline =
                        builder.preprocess()
                                .collapseCollections()
//                                .precision(targetCrs)
                                .build();
            } else  {
                pipeline = builder
                        .clip(isClip, false)
                        .simplify(isSimplify)
                        .collapseCollections()
//                        .precision(targetCrs)
                        .build();
            }
        } catch (FactoryException e) {
            e.printStackTrace();
        }

        return pipeline;
    }


    public Geometry onlyPolygon(Geometry result) {
        if ((result instanceof org.locationtech.jts.geom.Polygon) || (result instanceof MultiPolygon)) {
            return result;
        }
        List polys = org.locationtech.jts.geom.util.PolygonExtracter.getPolygons(result);
        if (polys.size() == 0) {
            return null;
        }
        if (polys.size() == 1) {
            return (org.locationtech.jts.geom.Polygon) polys.get(0);
        }
        return new MultiPolygon(
                (org.locationtech.jts.geom.Polygon[]) polys.toArray(new Polygon[polys.size()]), result.getFactory());
    }

    public Geometry union(Geometry g1, Geometry g2, double buffer, int depth, int maxDepth) {

        Geometry bg1;
        Geometry bg2;

        if (depth == 0) {
            bg1 = g1;
            bg2 = g2;
        } else {
            bg1 = g1.buffer(buffer);
            bg2 = g2.buffer(buffer);
        }

        try {
            return bg1.union(bg2);
        } catch (TopologyException e) {
            depth ++;
            if (depth > maxDepth) {
                return null;
            }
            return union(g1, g2, buffer*2, depth, maxDepth);
        }
    }


}
